﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Collections.ObjectModel;
using NthDownload.Common.Plugins;
using NthDownload.Composition;
using NthDownload.Contracts;
using System.ComponentModel;
using NthDownload.Agent;
using NthDownload.Agent.EventArguments;
using System.Threading.Tasks;
using System.Threading;
using NthDownload.Contracts.Plugins.Downloads;
using NthDownload.Contracts.Plugins.BandwidthProviders;
using NthDownload.Common.Mapping;

namespace NthDownload.BL
{
    [MappableObject]
    public class NthQueue : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion INotifyPropertyChanged

        #region Class Memebers

        private string _guid;
        private string _name;
        private bool _isSelected;

        private int _maxRunningDownloads;
        private int _maxRetries;
        private int _maxThreads;
        private bool _startDownloadsImmediately;

        private IBandwidthProvider _bandwidthProvider;

        private ObservableCollection<IDownload> _activeDownloads;
        private ObservableCollection<IDownload> _inactiveDownloads;
        private ObservableCollection<IDownload> _archivedDownloads;

        #endregion Class Memebers

        #region Constructors

        public NthQueue()
        { 
            //
            // Set defaults
            //
            DownloadQueue = new DownloadQueue();
            GUID = Guid.NewGuid().ToString();
            Name = "";
            BandwidthProvider = null;
            MaxRunningDownloads = 1;
            MaxRetries = 1;
            MaxThreads = 1;
            ActiveDownloads = new ObservableCollection<IDownload>();
            InactiveDownloads = new ObservableCollection<IDownload>();
            ArchivedDownloads = new ObservableCollection<IDownload>();
            
            SubscribeToDownloadQueueEvents();
            ImportService.TrySatisfyImports(this);
        }

        #endregion Constructors

        #region Properties

        /// <summary>
        /// Uniquely identifies a queue
        /// </summary>
        [MappableProperty]
        public string GUID
        {
            get
            {
                return _guid;
            }
            set
            {
                _guid = value;
                OnPropertyChanged("GUID");
            }
        }

        /// <summary>
        /// Name of the queue
        /// </summary>
        [MappableProperty]
        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }

        /// <summary>
        /// Indicates if the queue is selected
        /// </summary>
        public bool IsSelected
        {
            get
            {
                return _isSelected;
            }
            set
            {
                _isSelected = value;
                OnPropertyChanged("IsSelected");
            }
        }

        /// <summary>
        /// The bandwidth provider to use for this queue
        /// </summary>
        [MappableProperty(ReferenceClone = true)]
        public IBandwidthProvider BandwidthProvider
        {
            get
            {
                return _bandwidthProvider;
            }
            set
            {
                _bandwidthProvider = value;
                OnPropertyChanged("BandwidthProvider");
                DownloadQueue.BandwidthProvider = value;
            }
        }

        /// <summary>
        /// The maximum number of downloads that can be running at any 
        /// one time in this queue. 
        /// </summary>
        [MappableProperty]
        public int MaxRunningDownloads
        {
            get
            {
                return Math.Max(1, _maxRunningDownloads);
            }
            set
            {
                _maxRunningDownloads = Math.Max(1, value);
                OnPropertyChanged("MaxRunningDownloads");
                DownloadQueue.MaxRunningDownloads = value;
            }
        }

        /// <summary>
        /// The maximum number of retries downloads in this queue can
        /// encounter before stopping. 
        /// </summary>
        [MappableProperty]
        public int MaxRetries
        {
            get
            {
                return Math.Max(1, _maxRetries);
            }
            set
            {
                _maxRetries = Math.Max(1, value);
                OnPropertyChanged("MaxRetries");
                DownloadQueue.MaxRetries = value;
            }
        }

        /// <summary>
        /// The maximum number of threads downloads in this queue can
        /// use to download on.
        /// </summary>
        [MappableProperty]
        public int MaxThreads
        {
            get
            {
                return Math.Max(1, _maxThreads);
            }
            set
            {
                _maxThreads = Math.Max(1, value);
                OnPropertyChanged("MaxThreads");
                DownloadQueue.MaxThreads = value;
            }
        }

        /// <summary>
        /// Indicates if the queue should attempt to start downloads added 
        /// automatically.
        /// </summary>
        [MappableProperty]
        public bool StartDownloadsImmediately
        {
            get
            {
                return _startDownloadsImmediately;
            }
            set
            {
                _startDownloadsImmediately = value;
                OnPropertyChanged("StartDownloadsImmediately");
            }
        }

        /// <summary>
        /// The downloads that are currently active in this queue
        /// </summary>
        public ObservableCollection<IDownload> ActiveDownloads
        {
            get
            {
                return _activeDownloads;
            }
            set
            {
                _activeDownloads = value;
                OnPropertyChanged("ActiveDownloads");
            }
        }

        /// <summary>
        /// The downloads that are currently inactive in this queue
        /// </summary>
        public ObservableCollection<IDownload> InactiveDownloads
        {
            get
            {
                return _inactiveDownloads;
            }
            set
            {
                _inactiveDownloads = value;
                OnPropertyChanged("InactiveDownloads");
            }
        }

        /// <summary>
        /// The downloads that are archived for this queue
        /// </summary>
        public ObservableCollection<IDownload> ArchivedDownloads
        {
            get
            {
                return _archivedDownloads;
            }
            set
            {
                _archivedDownloads = value;
                OnPropertyChanged("ArchivedDownloads");
            }
        }

        /// <summary>
        /// Indicates if any of the downloads are running
        /// </summary>
        public bool HasRunningDownloads
        {
            get
            {
                return DownloadQueue.RunningDownloads.Count > 0;
            }
        }

        /// <summary>
        /// The Synchronization Context of the executing thread
        /// </summary>
        [Import(typeof(SynchronizationContext), AllowRecomposition = true)]
        public SynchronizationContext UISynchronizationContext { get; set; }

        #endregion Properties

        #region Private Properties

        /// <summary>
        /// The instance of the download queue used to process downloads
        /// </summary>
        private DownloadQueue DownloadQueue { get; set; }

        #endregion Private Properties

        #region Methods

        /// <summary>
        /// Adds downloads to queue.
        /// </summary>
        /// <param name="downloads">The downloads to be added to the queue.</param>
        /// <param name="startTask">The task which starts the newly created downloads.</param>
        public Task<QueueOperation> AddDownloads(IEnumerable<IDownload> downloads, out Task<QueueOperation> startTask)
        {
            //
            // Add to download queue
            //
            Task<QueueOperation> addTask = DownloadQueue.AddDownloads(downloads, out startTask);

            //
            // Maintain views
            //
            Task<QueueOperation> viewMaintenanceTask = addTask.ContinueWith(t =>
                {
                    if (t.Exception == null)
                    {
                        List<IDownload> addedDownloads = t.Result.DownloadErrors.Where(k => k.Value == null).Select(k => k.Key).ToList();
                        RemoveFromInactiveDownloads(addedDownloads);
                        RemoveFromArchivedDownloads(addedDownloads);

                        List<IDownload> activeDownloads = ActiveDownloads.ToList();
                        AddToActiveDownloads(addedDownloads.Except(activeDownloads).ToList(), false);
                    }
                    else
                    {
                        //
                        // Rethrow exception, this ensures it'll bubble up to any further ContinueWith chained off this task
                        //
                        throw t.Exception;
                    }

                    return t.Result;
                });

            return viewMaintenanceTask;
        }

        /// <summary>
        /// Removes downloads from the queue. If any are running they are aborted first.
        /// </summary>
        /// <param name="downloads">The downloads to remove</param>
        public Task<QueueOperation> RemoveDownloads(IEnumerable<IDownload> downloads)
        {
            //
            // Get downloads that need to be stopped
            //
            DownloadProcessor downloadProcessor;
            IEnumerable<IDownload> downloadsToStop = downloads.Where(d => DownloadQueue.RunningDownloads.TryGetValue(d.GUID, out downloadProcessor));

            //
            // Remove downloads that aren't running
            //
            Task<QueueOperation> removeNonRunningDownloadsTask = DownloadQueue.RemoveDownloads(downloads.Except(downloadsToStop));

            //
            // Stop running downloads
            //
            Task<QueueOperation> stopTask = removeNonRunningDownloadsTask.ContinueWith<QueueOperation>(t =>
                {
                    Task<QueueOperation> st;

                    if (t.Exception == null)
                    {
                        RemoveFromAllDownloads(downloads.Except(downloadsToStop).ToList());

                        st = DownloadQueue.StopDownloads(downloadsToStop);
                        st.Wait();

                        List<IDownload> stoppedDownloads = st.Result.DownloadErrors.Where(k => k.Value == null).Select(k => k.Key).ToList();
                        RemoveFromAllDownloads(stoppedDownloads);
                    }
                    else
                    {
                        //
                        // Rethrow exception, this ensures it'll bubble up to any further ContinueWith chained off this task
                        //
                        throw t.Exception;
                    }

                    return st.Result;
                });
             

            //
            // Remove downloads
            //
            Task<QueueOperation> removeStoppedDownloadsTask = stopTask.ContinueWith(t =>
                {
                    Task<QueueOperation> rt;

                    if (t.Exception == null)
                    {
                        IEnumerable<IDownload> stoppedDownloads = t.Result.DownloadErrors.Where(k => k.Value == null).Select(k => k.Key);
                        rt = DownloadQueue.RemoveDownloads(stoppedDownloads);
                        rt.Wait();

                        List<IDownload> removedDownloads = rt.Result.DownloadErrors.Where(k => k.Value == null).Select(k => k.Key).ToList();
                        RemoveFromAllDownloads(removedDownloads);
                    }
                    else
                    {
                        //
                        // Rethrow exception, this ensures it'll bubble up to any further ContinueWith chained off this task
                        //
                        throw t.Exception;
                    }

                    return rt.Result;
                });

            return removeStoppedDownloadsTask;
        }

        /// <summary>
        /// Starts processing downloads. If any of the the provided downloads aren't 
        /// in the queue they are added. If the max number of running downloads has
        /// been reached the downloads are queued.
        /// </summary>
        /// <param name="downloads">The downloads to start</param>
        public Task<QueueOperation> StartDownloads(IEnumerable<IDownload> downloads)
        {
            //
            // Start downloads
            //
            Task<QueueOperation> startTask = DownloadQueue.StartDownloads(downloads);

            //
            // Maintain views
            //
            Task<QueueOperation> viewMaintenanceTask = startTask.ContinueWith(t =>
            {
                if (t.Exception == null)
                {
                    List<IDownload> startedDownloads = t.Result.DownloadErrors.Where(k => k.Value == null).Select(k => k.Key).Where(d => d.Status == DownloadStatuses.Downloading || d.Status == DownloadStatuses.Queued).ToList();
                    RemoveFromInactiveDownloads(startedDownloads);
                    RemoveFromArchivedDownloads(startedDownloads);
                    AddToActiveDownloads(startedDownloads.Except(ActiveDownloads).ToList(), false);
                }
                else
                {
                    //
                    // Rethrow exception, this ensures it'll bubble up to any further ContinueWith chained off this task
                    //
                    throw t.Exception;
                }

                return t.Result;
            });

            return viewMaintenanceTask;
        }

        /// <summary>
        /// Starts downloads in the queue until the max number of running downloads 
        /// is reached. This method only starts downloads which are ready to start, 
        /// it will not act upon the downloads which aren't ready to start.
        /// A download is ready to start when it isn't running and it's status
        /// is Queued or Downloading.
        /// </summary>
        public Task<QueueOperation> StartQueuedDownloads()
        {
            return DownloadQueue.StartQueuedDownloads();
        }

        /// <summary>
        /// Pauses given downloads.
        /// </summary>
        /// <param name="downloads">The downloads to pause.</param>
        public Task<QueueOperation> PauseDownloads(IEnumerable<IDownload> downloads)
        {
            return DownloadQueue.PauseDownloads(downloads);
        }

        /// <summary>
        /// Stops given downloads.
        /// </summary>
        /// <param name="downloads">The downloads to stop.</param>
        public Task<QueueOperation> StopDownloads(IEnumerable<IDownload> downloads)
        {
            return DownloadQueue.StopDownloads(downloads);
        }

        /// <summary>
        /// Stops given downloads.
        /// </summary>
        /// <param name="downloads">The downlaods to cancel</param>
        public Task<QueueOperation> CancelDownloads(IEnumerable<IDownload> downloads)
        {
            return DownloadQueue.CancelDownloads(downloads);
        }

        /// <summary>
        /// Moves downloads up in the queue.
        /// </summary>
        /// <param name="downloads">The downloads to to move up.</param>
        public Task<QueueOperation> MoveDownloadsUp(IEnumerable<IDownload> downloads)
        {
            return DownloadQueue.MoveDownloadsUp(downloads);
        }

        /// <summary>
        /// Moves downloads down in the queue.
        /// </summary>
        /// <param name="downloads">The downloads to move down.</param>
        public Task<QueueOperation> MoveDownloadsDown(IEnumerable<IDownload> downloads)
        {
            return DownloadQueue.MoveDownloadsDown(downloads);
        }

        #endregion Methods

        #region Private Methods

        /// <summary>
        /// Subscribles to the download queues download events
        /// </summary>
        private void SubscribeToDownloadQueueEvents()
        {
            DownloadQueue.DownloadFinished -= DownloadQueueDownloadFinished;
            DownloadQueue.DownloadFinished += DownloadQueueDownloadFinished;

            DownloadQueue.DownloadPaused -= DownloadQueueDownloadPaused;
            DownloadQueue.DownloadPaused += DownloadQueueDownloadPaused;

            DownloadQueue.DownloadStarted -= DownloadQueueDownloadStarted;
            DownloadQueue.DownloadStarted += DownloadQueueDownloadStarted;

            DownloadQueue.DownloadStopped -= DownloadQueueDownloadStopped;
            DownloadQueue.DownloadStopped += DownloadQueueDownloadStopped;
        }

        /// <summary>
        /// Removes downloads from all views
        /// </summary>
        /// <param name="downloads">The downloads to remove.</param>
        private void RemoveFromAllDownloads(IList<IDownload> downloads)
        {
            //
            // Remove all instances of the downloads
            //
            RemoveFromActiveDownloads(downloads);
            RemoveFromInactiveDownloads(downloads);
            RemoveFromArchivedDownloads(downloads);
        }

        #endregion Private Methods

        #region Internal Methods

        /// <summary>
        /// Add downloads to active downloads view.
        /// </summary>
        /// <param name="downloads">The downloads to add.</param>
        /// <param name="fixStatuses">Indicates if the status of the download should be changed if it indicates the download is being processed.</param>
        internal void AddToActiveDownloads(IList<IDownload> downloads, bool fixStatuses)
        {
            UISynchronizationContext.Send(delegate
            {
                foreach (IDownload item in downloads)
                {
                    ActiveDownloads.Add(item);
                    if (fixStatuses) item.FixStatus(DownloadStatuses.Ready, DownloadStatuses.Ready, DownloadStatuses.Ready);
                }
            }, null);
        }

        /// <summary>
        /// Removes downloads from active downloads view.
        /// </summary>
        /// <param name="downloads">The downloads to remove.</param>
        internal void RemoveFromActiveDownloads(IList<IDownload> downloads)
        {
            UISynchronizationContext.Send(delegate
            {
                foreach (IDownload item in downloads)
                {
                    ActiveDownloads.Remove(item);
                }
            }, null);
        }

        /// <summary>
        /// Add downloads to inactive downloads view.
        /// </summary>
        /// <param name="downloads">The downloads to add.</param>
        /// <param name="fixStatuses">Indicates if the status of the download should be changed if it indicates the download is being processed.</param>
        internal void AddToInactiveDownloads(IList<IDownload> downloads, bool fixStatuses)
        {
            UISynchronizationContext.Send(delegate
            {
                foreach (IDownload item in downloads)
                {
                    InactiveDownloads.Add(item);
                    if (fixStatuses) item.FixStatus(DownloadStatuses.Error, DownloadStatuses.Finished, DownloadStatuses.Ready);
                }
            }, null);
        }

        /// <summary>
        /// Removes downloads from inactive downloads view.
        /// </summary>
        /// <param name="downloads">The downloads to remove.</param>
        internal void RemoveFromInactiveDownloads(IList<IDownload> downloads)
        {
            UISynchronizationContext.Send(delegate
            {
                foreach (IDownload item in downloads)
                {
                    InactiveDownloads.Remove(item);
                }
            }, null);
        }

        /// <summary>
        /// Add downloads to archived downloads view.
        /// </summary>
        /// <param name="downloads">The downloads to add.</param>
        /// <param name="fixStatuses">Indicates if the status of the download should be changed if it indicates the download is being processed.</param>
        internal void AddToArchivedDownloads(IList<IDownload> downloads, bool fixStatuses)
        {
            UISynchronizationContext.Send(delegate
            {
                foreach (IDownload item in downloads)
                {
                    ArchivedDownloads.Add(item);
                    if (fixStatuses) item.FixStatus(DownloadStatuses.Error, DownloadStatuses.Finished, DownloadStatuses.Ready);
                }
            }, null);
        }

        /// <summary>
        /// Removes downloads from archived downloads view.
        /// </summary>
        /// <param name="downloads">The download to remove.</param>
        internal void RemoveFromArchivedDownloads(IList<IDownload> downloads)
        {
            UISynchronizationContext.Send(delegate
            {
                foreach (IDownload item in downloads)
                {
                    ArchivedDownloads.Remove(item);
                }
            }, null);
        }

        #endregion Internal Methods

        #region Download Queue Event Handlers

        /// <summary>
        /// Download queue download stopped event handler
        /// </summary>
        private void DownloadQueueDownloadStopped(object sender, DownloadOperationCompletedEventArgs e)
        {
            //
            // If download stopped with out error do nothing
            //
            if (e.Error == null)
            {
                return;
            }

            //
            // Remove from active downloads and queue
            //
            List<IDownload> downloads = new List<IDownload> { e.Download };
            RemoveFromActiveDownloads(downloads);
            DownloadQueue.RemoveDownloads(downloads);

            //
            // Add to inactive downloads
            //
            AddToInactiveDownloads(downloads, false);

            //
            // Raise property changed events for effected properties
            //
            OnPropertyChanged("HasRunningDownloads");
        }

        /// <summary>
        /// Download queue download started event handler
        /// </summary>
        private void DownloadQueueDownloadStarted(object sender, DownloadOperationCompletedEventArgs e)
        {
            //
            // Raise property changed events for effected properties
            //
            OnPropertyChanged("HasRunningDownloads");
        }

        /// <summary>
        /// Download queue download paused event handler
        /// </summary>
        private void DownloadQueueDownloadPaused(object sender, DownloadOperationCompletedEventArgs e)
        {
            //
            // Raise property changed events for effected properties
            //
            OnPropertyChanged("HasRunningDownloads");
        }

        /// <summary>
        /// Download queue download finished event handler
        /// </summary>
        private void DownloadQueueDownloadFinished(object sender, DownloadOperationCompletedEventArgs e)
        {
            //
            // Remove from active downloads and queue
            //
            List<IDownload> downloads = new List<IDownload> { e.Download };
            RemoveFromActiveDownloads(downloads);
            DownloadQueue.RemoveDownloads(downloads);

            //
            // Add to archived downloads downloads
            //
            AddToArchivedDownloads(downloads, false);

            //
            // Raise property changed events for effected properties
            //
            OnPropertyChanged("HasRunningDownloads");
        }

        #endregion Download Queue Event Handlers
    }
}
